import { ReloadOptions, router } from '@inertiajs/core'
import { createElement, ReactNode, useCallback, useEffect, useRef, useState } from 'react'
import usePage from './usePage'

interface WhenVisibleProps {
  children: ReactNode | (() => ReactNode)
  fallback: ReactNode | (() => ReactNode)
  data?: string | string[]
  params?: ReloadOptions
  buffer?: number
  as?: string
  always?: boolean
}

const WhenVisible = ({ children, data, params, buffer, as, always, fallback }: WhenVisibleProps) => {
  always = always ?? false
  as = as ?? 'div'
  fallback = fallback ?? null

  const [loaded, setLoaded] = useState(false)
  const fetching = useRef<boolean>(false)
  const ref = useRef<HTMLDivElement>(null)
  const observer = useRef<IntersectionObserver | null>(null)

  const page = usePage()

  useEffect(() => {
    if (Array.isArray(data)) {
      // For arrays, reset loaded if any prop becomes undefined
      if (data.some((key) => page.props[key] === undefined)) {
        setLoaded(false)
      }
    } else if (data) {
      // For single prop, reset loaded if prop becomes undefined
      if (page.props[data] === undefined) {
        setLoaded(false)
      }
    }
  }, [data, ...(Array.isArray(data) ? data.map((key) => page.props[key]) : [page.props[data!]])])

  const getReloadParams = useCallback<() => Partial<ReloadOptions>>(() => {
    if (data) {
      return {
        only: (Array.isArray(data) ? data : [data]) as string[],
      }
    }

    if (!params) {
      throw new Error('You must provide either a `data` or `params` prop.')
    }

    return params
  }, [params, data])

  const registerObserver = () => {
    observer.current?.disconnect()

    observer.current = new IntersectionObserver(
      (entries) => {
        if (!entries[0].isIntersecting) {
          return
        }

        if (fetching.current) {
          return
        }

        if (!always && loaded) {
          return
        }

        fetching.current = true

        const reloadParams = getReloadParams()

        router.reload({
          ...reloadParams,
          onStart: (e) => {
            fetching.current = true
            reloadParams.onStart?.(e)
          },
          onFinish: (e) => {
            setLoaded(true)
            fetching.current = false
            reloadParams.onFinish?.(e)

            if (!always) {
              observer.current?.disconnect()
            }
          },
        })
      },
      {
        rootMargin: `${buffer || 0}px`,
      },
    )

    observer.current.observe(ref.current!)
  }

  useEffect(() => {
    if (!ref.current) {
      return
    }

    registerObserver()

    return () => {
      observer.current?.disconnect()
    }
  }, [loaded, ref, getReloadParams, buffer])

  const resolveChildren = () => (typeof children === 'function' ? children() : children)
  const resolveFallback = () => (typeof fallback === 'function' ? fallback() : fallback)

  if (always || !loaded) {
    return createElement(
      as,
      {
        props: null,
        ref,
      },
      loaded ? resolveChildren() : resolveFallback(),
    )
  }

  return loaded ? resolveChildren() : null
}

WhenVisible.displayName = 'InertiaWhenVisible'

export default WhenVisible
